/**
 * Copyright 2021 xpstem.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.xpstem.lelink.connection;

import com.xpstem.lelink.util.ByteUtil;
import com.xpstem.lelink.util.StrUtil;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramPacket;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.*;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.ByteBufFormat;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.*;
import java.nio.ByteOrder;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * wifi connection
 * @author billy zhang
 */
public class WifiConnection extends BaseConnection {

    private static final Logger log = LoggerFactory.getLogger(WifiConnection.class);

    private Bootstrap client = null;
    private ChannelFuture clientFuture = null;

    private InetAddress serverAddr;
    private int serverPort;
    private String serialNumber;
    private String serverName;
    private String protocol;

    private static final int MAX_RETRY = 5;

    private boolean unlock = false;
    private CountDownLatch unlockLatch;

    @Override
    protected int doConnect() throws IOException {

        findServer();
        connectServer();
        return 0;

    }

    private void findServer() {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .channel(NioDatagramChannel.class)
                    .option(ChannelOption.SO_BROADCAST, true)
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
                    .handler(new BroadcastHandler());

            Channel channel = bootstrap.bind(3015).syncUninterruptibly().channel();
            channel.closeFuture().sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        } finally {
            group.shutdownGracefully();
        }

    }

    public class BroadcastHandler extends SimpleChannelInboundHandler<DatagramPacket> {

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {

            ByteBuf byteBuf = msg.copy().content();
            byte[] bytes = new byte[byteBuf.readableBytes()];
            byteBuf.readBytes(bytes);

            String broadcastMsg = new String(bytes);
            Pattern broadcasePattern = Pattern.compile(
                    "Serial-Number:\\s(\\w*)\\s\\n" +
                            "Port:\\s(\\d{4,4})\\s\\n" +
                            "Name:\\s(\\w+)\\s\\n" +
                            "Protocol:\\s(\\w+)\\s\\n");
            Matcher matcher = broadcasePattern.matcher(broadcastMsg);
            if (!matcher.matches()) {
                log.warn("Unexpected Broadcast message: " + broadcastMsg);
                return;
            }

            serverAddr = msg.sender().getAddress();
            serialNumber = matcher.group(1);
            serverPort = Integer.parseInt(matcher.group(2));
            serverName = matcher.group(3);
            protocol = matcher.group(4);

            ByteBuf byteBuf2 = Unpooled.copiedBuffer(new byte[1]);
            ctx.writeAndFlush(new DatagramPacket(byteBuf2, msg.sender()));

            ctx.close();
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            log.warn("broadcast message handle error.", cause);
        }
    }

    private void connectServer() {

        EventLoopGroup group = new NioEventLoopGroup();
        client = new Bootstrap();
        client.group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel channel) throws Exception {
                        channel.pipeline().addLast(new LoggingHandler(ByteBufFormat.HEX_DUMP));
                        channel.pipeline().addLast(new StringEncoder());
                        channel.pipeline().addLast(new StringDecoder());
                        channel.pipeline().addLast(new UnlockHandler());
                    }
                });

        clientFuture = client.connect(serverAddr, serverPort).addListener(future -> {
            if (!future.isSuccess()) {
                connectTry(MAX_RETRY);
            }
        });

        try {
            clientFuture.sync();
            unlockLatch = new CountDownLatch(1);
            unlockLatch.await(5, TimeUnit.SECONDS);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
            Thread.currentThread().interrupt();
        }
    }

    private void connectTry(int retry) throws IOException {
        clientFuture = client.connect(serverAddr, serverPort).addListener(future -> {
            if (retry == 0) {
                throw new IOException("connect server timeout.");
            } else {
                int order = (MAX_RETRY - retry) + 1;
                int delay = 1 << order;
                client.config().group().schedule(
                        () -> {
                            try {
                                connectTry(retry - 1);
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }, delay, TimeUnit.SECONDS);
            }
        });
    }

    @Override
    protected void doSend(byte[] data) throws IOException {
        if (!unlock) {
            throw new IOException("No connection established .");
        }

        ByteBuf buf = Unpooled.copiedBuffer(data);
        clientFuture.channel().writeAndFlush(buf);

        String hexData = StrUtil.toHexWithSep(data, "_");
        log.info("{} send message: {}", clientFuture.channel().id(), hexData);

    }

    public class UnlockHandler extends SimpleChannelInboundHandler<String> {

        @Override
        public void channelActive(ChannelHandlerContext context) {
            String unlockMsg = "GET /target?sn=" + serialNumber + " VMTP1.0 Protocol: " + protocol + "\n";
            context.channel().writeAndFlush(unlockMsg);
            log.debug( "{} send message: {}", context.channel().id(), unlockMsg);
        }

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {

            log.debug(  "{} received message: {}", ctx.channel().id(), msg);
            if (msg.startsWith("Accept:EV340")) {
                unlock = true;
                unlockLatch.countDown();
            } else {
                log.warn("No wifi connection established {}", serverName);
            }

            ctx.pipeline().remove(this);
            ctx.pipeline().remove(StringEncoder.class);
            ctx.pipeline().remove(StringDecoder.class);

            ctx.pipeline().addFirst(new LengthFieldBasedFrameDecoder(
                    ByteOrder.LITTLE_ENDIAN, Integer.MAX_VALUE, 0, 2, 0, 0, false));
            ctx.pipeline().addLast(new NettyClientHandler());
        }
    }

    public class NettyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

        @Override
        public void channelRead0(ChannelHandlerContext context, ByteBuf msg) throws Exception {

            ByteBuf buf = Unpooled.copiedBuffer(msg);
            String hexMsg = StrUtil.toHexWithSep(buf.array(), "_");
            log.info( "{} received message: {}", context.channel().id(), hexMsg);

            //byte0
            buf.readByte();
            //byte1
            buf.readByte();

            byte[] data = new byte[buf.readableBytes()];
            buf.readBytes(data);

            int counter = (ByteUtil.byteToInt(data[1])<<8) + ByteUtil.byteToInt(data[0]);
            response(counter, data);
        }

    }

    @Override
    public void disconnect() throws IOException {
        try {
            clientFuture.channel().close().sync();
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }
}
